route.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. /**
  2. * First-party Briven Auth proxy (auth-core FDI).
  3. *
  4. * Browser → http://localhost:3000/api/auth/…
  5. * Upstream → https://api.briven.tech/v1/auth-core/fdi/…
  6. *
  7. * @briven/auth SDK builds: {apiOrigin}/v1/auth-core/fdi/signinup/code
  8. * With apiOrigin = same-origin + "/api/auth", full path is:
  9. * /api/auth/v1/auth-core/fdi/signinup/code
  10. * We strip a leading v1/auth-core/fdi/ so we never double the prefix.
  11. */
  12. import { NextRequest, NextResponse } from "next/server";
  13. import {
  14. brivenUpstreamOrigin,
  15. collectSetCookies,
  16. rewriteSetCookieForFirstParty,
  17. } from "@/lib/auth-proxy";
  18. export const dynamic = "force-dynamic";
  19. export const runtime = "nodejs";
  20. function runtimeEnv(name: string): string {
  21. return (process.env[name] ?? "").trim();
  22. }
  23. type RouteCtx = { params: Promise<{ path: string[] }> };
  24. async function proxy(req: NextRequest, ctx: RouteCtx): Promise<Response> {
  25. const { path: segments } = await ctx.params;
  26. let path = (segments ?? []).join("/");
  27. if (path.includes("..")) {
  28. return NextResponse.json({ ok: false, error: "invalid auth path" }, { status: 400 });
  29. }
  30. // SDK may send full FDI prefix under /api/auth
  31. path = path.replace(/^v1\/auth-core\/fdi\/?/, "");
  32. path = path.replace(/^v1\/auth-tenant\/?/, "");
  33. path = path.replace(/^v1\/auth-core\/session\/me\/?$/, "session/me");
  34. const incomingUrl = new URL(req.url);
  35. // session/me lives outside /fdi/* (gold path); get-session is legacy name
  36. const isSessionMe = path === "session/me" || path === "get-session";
  37. const target = isSessionMe
  38. ? `${brivenUpstreamOrigin()}/v1/auth-core/session/me${incomingUrl.search}`
  39. : `${brivenUpstreamOrigin()}/v1/auth-core/fdi/${path}${incomingUrl.search}`;
  40. const headers = new Headers();
  41. const pass = [
  42. "content-type",
  43. "cookie",
  44. "authorization",
  45. "x-briven-project-id",
  46. "rid",
  47. "fdi-version",
  48. "st-auth-mode",
  49. "anti-csrf",
  50. "user-agent",
  51. "referer",
  52. "x-forwarded-for",
  53. "x-real-ip",
  54. "cf-connecting-ip",
  55. ] as const;
  56. for (const name of pass) {
  57. const v = req.headers.get(name);
  58. if (v) headers.set(name, v);
  59. }
  60. if (!headers.has("authorization")) {
  61. const pk =
  62. runtimeEnv("BRIVEN_AUTH_PUBLIC_KEY") ||
  63. runtimeEnv("NEXT_PUBLIC_BRIVEN_AUTH_KEY");
  64. if (pk.startsWith("pk_briven_auth_")) {
  65. headers.set("authorization", `Bearer ${pk}`);
  66. }
  67. }
  68. if (!headers.has("x-briven-project-id")) {
  69. const project =
  70. runtimeEnv("BRIVEN_PROJECT_ID") ||
  71. runtimeEnv("NEXT_PUBLIC_BRIVEN_PROJECT_ID");
  72. if (project.startsWith("p_")) {
  73. headers.set("x-briven-project-id", project);
  74. }
  75. }
  76. const clientIp =
  77. req.headers.get("cf-connecting-ip")?.trim() ||
  78. req.headers.get("x-real-ip")?.trim() ||
  79. req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
  80. "";
  81. if (clientIp) {
  82. headers.set("x-briven-client-ip", clientIp);
  83. if (!headers.has("x-real-ip")) headers.set("x-real-ip", clientIp);
  84. if (!headers.has("x-forwarded-for")) headers.set("x-forwarded-for", clientIp);
  85. }
  86. const origin = req.headers.get("origin") || incomingUrl.origin;
  87. if (origin) headers.set("origin", origin);
  88. // Ensure project id on query for engines that read it there
  89. const targetUrl = new URL(target);
  90. const project =
  91. headers.get("x-briven-project-id") ||
  92. runtimeEnv("NEXT_PUBLIC_BRIVEN_PROJECT_ID");
  93. if (project && !targetUrl.searchParams.has("briven_project_id")) {
  94. targetUrl.searchParams.set("briven_project_id", project);
  95. }
  96. const method = req.method.toUpperCase();
  97. const hasBody = method !== "GET" && method !== "HEAD";
  98. let upstream: Response;
  99. try {
  100. upstream = await fetch(targetUrl.toString(), {
  101. method,
  102. headers,
  103. body: hasBody ? await req.arrayBuffer() : undefined,
  104. redirect: "manual",
  105. });
  106. } catch (err) {
  107. const message = err instanceof Error ? err.message : "upstream unreachable";
  108. return NextResponse.json(
  109. {
  110. ok: false,
  111. code: "network_error",
  112. message: `Auth proxy could not reach Briven: ${message}`,
  113. },
  114. { status: 502 },
  115. );
  116. }
  117. const outHeaders = new Headers();
  118. for (const name of [
  119. "content-type",
  120. "cache-control",
  121. "location",
  122. "x-request-id",
  123. "x-briven-session-handle",
  124. ]) {
  125. const v = upstream.headers.get(name);
  126. if (v) outHeaders.set(name, v);
  127. }
  128. for (const sc of collectSetCookies(upstream.headers)) {
  129. outHeaders.append("set-cookie", rewriteSetCookieForFirstParty(sc));
  130. }
  131. return new Response(upstream.body, {
  132. status: upstream.status,
  133. statusText: upstream.statusText,
  134. headers: outHeaders,
  135. });
  136. }
  137. export const GET = proxy;
  138. export const POST = proxy;
  139. export const PUT = proxy;
  140. export const PATCH = proxy;
  141. export const DELETE = proxy;
  142. export const HEAD = proxy;
  143. export const OPTIONS = proxy;